Skip to content

S01-09 Servlet-JSP ​

[TOC]

概述 ​

核心概念 ​

JSP(JavaServer Pages) 是一种构建在 Servlet 规范之上的服务端动态网页生成技术。开发人员编写的 .jsp 文件存放在 Web 应用目录中,当客户端发起请求时,服务端容器会动态解析并将其转换为纯标准的 HTML、CSS 和 JavaScript 文本流,最终交由浏览器解析渲染。

核心价值:

  • 关注点分离:允许界面排版与服务端业务代码拆分,便于前后端角色分工。
  • 复用 Java 生态:能够无缝调用 Java 基础类库、第三方框架以及企业级业务组件(如 JavaBean)。
  • 跨平台部署:遵循统一的 Java EE / Jakarta EE 规范,可在所有兼容的 Servlet 容器(如 Tomcat、Jetty、WebLogic)中无缝迁移。

技术背景 ​

诞生背景 ​

在 JavaWeb 发展早期,动态页面完全依赖 Servlet 生成。开发人员需要在 Java 类中通过字符输出流逐行拼接 HTML 标签:

java
// 早期 Servlet 拼接 HTML 的典型方式
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
  resp.setContentType("text/html;charset=UTF-8");
  PrintWriter out = resp.getWriter();
  out.println("<html><body><h1>Hello World</h1></body></html>");
}

这种开发方式存在显著弊端:页面结构复杂时拼接极易出错,前端设计人员无法直接调试界面,且视图与控制逻辑深度耦合。

为了应对这一挑战,Sun 公司于 1999 年推出了 JSP(JavaServer Pages)规范,将模式反转为在 HTML 模板中内嵌 Java 逻辑。

技术对比 ​

Servlet 与 JSP 在底层同根同源,但在分工上各有侧重:

维度ServletJSP
代码形态纯 Java 代码,内部通过字符串嵌入 HTMLHTML 文本为主,内部通过标签或脚本嵌入 Java
核心职责控制器(Controller),专注请求分发与业务调度视图层(View),专注数据展示与界面渲染
维护难度修改界面展示需要重新编译部署 Java 类修改界面仅需保存静态模板,容器自动重新编译

运行机理 ​

转换流程 ​

JSP 并非由浏览器直接读取,也不是在服务端进行简单的文本替换,而是遵循严格的翻译与编译流程:

  1. 客户端发起请求:浏览器向 Web 服务器发出访问 .jsp 页面的 HTTP 请求。

  2. 源码翻译:Web 容器(如 Tomcat 中的 Jasper 引擎)定位目标文件,将其解析并翻译为一个完整的 .java 源文件(继承自 HttpJspBase,即标准的 Servlet)。

  3. 字节码编译:容器调用 Java 编译器,将 .java 文件编译为可在 JVM 中执行的 .class 字节码文件。

  4. 加载与实例化:Servlet 引擎加载生成的字节码,在内存中创建唯一的 Servlet 实例。

  5. 服务执行:容器分配处理线程并调用其核心服务方法,静态 HTML 作为字符常量输出,内嵌的 Java 逻辑被动态计算并写入响应流。

  6. 响应返回:生成纯粹的 HTML 响应体,经由 HTTP 协议回传给浏览器展示。

    java
    // 翻译生成的 Servlet 核心方法结构
    // 页面中的所有静态文本被自动转换为流输出
    public void _jspService(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
      response.setContentType("text/html;charset=UTF-8");
      JspWriter out = pageContext.getOut();
      out.write("<html><body><h1>Dynamic Content</h1></body></html>");
    }

生命周期 ​

作为一种特殊的 Servlet,JSP 具有明确的生命周期状态:

  1. 初始化(jspInit):在 JSP 类实例化后由容器调用一次,用于分配外部资源或读取初始参数。

  2. 请求处理(_jspService):针对每一次客户端请求,容器都会在独立线程中调用此方法,接收请求对象并执行具体的页面渲染逻辑。

  3. 销毁(jspDestroy):当 Web 应用重新部署或服务器正常关闭时触发,用于释放数据库连接或清理内存句柄。

架构演进 ​

Model 1 模式 ​

在 JSP 推广初期,普遍采用 Model 1 模式。该模式下页面完全以 JSP 为核心,JSP 不仅负责接收请求、读取请求参数,还直接负责实例化 JavaBean 处理数据库查询,并最终输出页面。这种模式在小型项目中开发快速,但在业务复杂后会导致页面代码臃肿,难以维护。

Model 2 模式 ​

为了解决 Model 1 的混乱问题,JavaWeb 演化出标准的 Model 2(即经典的 MVC 设计模式):

  • Model(模型):由 JavaBean、Service 和 DAO 组成,专职处理业务逻辑与持久化操作。
  • View(视图):由 JSP 担任,只负责从作用域中提取数据并完成视觉渲染,杜绝复杂业务代码。
  • Controller(控制器):由 Servlet 担任,负责解析客户端请求、调用模型处理逻辑,最后决定将请求转发给哪一个 JSP 视图。

image-20260921114458094

基础语法 ​

语法概述 ​

语法构成:JSP 文件的语法核心在于动静结合。一个标准的 JSP 文件通常包含五大核心元素:

  • 模板元素:HTML、CSS、JavaScript 静态文本。
  • 脚本元素:用于在页面中编写 Java 业务逻辑。
  • 注释元素:区分客户端与服务端的开发注释。
  • 指令元素:向 JSP 引擎传递页面环境与编译指示。
  • 动作元素:通过 XML 风格的标签实现动态调度与组件复用。

模板元素 ​

模板元素:即 HTML、CSS 或纯文本代码。JSP 引擎在遇到模板元素时不会对其做任何语法分析,而是将其原封不动地翻译为 Servlet 中的字符输出语句。

java
// 翻译生成的 Servlet 中的输出形式
out.write("<!DOCTYPE html>\n<html>\n<body>\n");

脚本元素 ​

脚本元素:用于在 HTML 页面中嵌入 Java 代码。按底层作用域和书写位置,分为三大类别。

分类1:声明脚本 ​

语法格式为 <%! ... %>。

  • 底层机制:声明的代码会被直接放置在生成的 Servlet 类体中、方法体之外,相当于类的成员变量或成员方法。

  • 线程风险:由于 Servlet 在容器中默认是单例多线程运行的,声明脚本中的成员变量被所有并发请求共享,修改该变量存在线程安全隐患。

    jsp
    <%-- 声明成员变量与全局计数方法 --%>
    <%!
      private int count = 0;
      public synchronized int getCount() {
        return ++count;
      }
    %>

分类2:代码片段 ​

语法格式为 <% ... %>(常称 Scriptlet)。

  • 底层机制:代码会被直接放置在 Servlet 的核心服务方法 _jspService() 内部。

  • 变量特性:在此处声明的变量均为局部变量,每次请求调用该方法都会独立分配栈帧,不存在多线程共享冲突。

    jsp
    <%-- 代码片段嵌入循环逻辑生成列表 --%>
    <%
      for (int i = 1; i <= 3; i++) {
        out.println("<li>Item: " + i + "</li>");
      }
    %>

分类3:表达式脚本 ​

语法格式为 <%= ... %>。

  • 底层机制:容器会将表达式的结果转换为 out.print(...) 自动输出到页面响应流中。

  • 书写规范:表达式末尾绝对不能加分号,且必须拥有可被计算为具体值的表达式或变量。

    jsp
    <%-- 表达式脚本直接计算并输出结果 --%>
    <p>用户名:<%= "guest".toUpperCase() %></p>

脚本对比 ​

脚本类型语法格式翻译后的代码位置变量/逻辑属性线程安全性
声明脚本<%! ... %>Servlet 类体内部、方法体外部成员变量 / 成员方法存在多线程安全隐患
代码片段<% ... %>_jspService() 方法体内部局部变量 / 业务控制流线程安全(局部栈帧)
表达式脚本<%= ... %>_jspService() 中的 out.print(...)表达式求值并直接页面输出线程安全

注释元素 ​

JSP 支持三种不同生命周期的注释方式。

注释类型 ​

  1. JSP 注释(<%-- 注释内容 --%>):服务端专有注释。JSP 引擎在源码翻译阶段会彻底忽略此内容,既不会编译到 .class 文件中,也不会输出到客户端响应中。

  2. HTML 注释(<!-- 注释内容 -->):客户端注释。作为模板元素原样输出,在生成的页面源码中对浏览器可见。

  3. Java 注释(// 或 /* ... */):仅在 <% ... %> 或 <%! ... %> 内部生效,会保留在编译生成的 .java 源码中,但不会向浏览器输出。

可见性对比 ​

注释类型语法示例翻译后的 .java 文件浏览器查看源码
JSP 注释<%-- 业务描述 --%>不可见(已剔除)不可见
HTML 注释<!-- 结构标记 -->存在于 out.write() 参数中完全可见
Java 注释// 单行逻辑可见不可见

指令元素 ​

指令概述 ​

基本概念 ​

JSP 指令(Directives) 是为 JSP 引擎(如 Tomcat 的 Jasper)设计的特殊标记。指令本身不会直接产生任何可见的 HTML 输出,而是在 JSP 页面被翻译成 Servlet 源代码的阶段,向容器传递编译指示、设置全局属性或引入外部资源。

语法规则 ​

指令统一使用 <%@ ... %> 标记包裹,其通用书写规范如下:

jsp
<%-- JSP 指令标准语法格式模板 --%>
<%@ directive_name attribute1="value1" attribute2="value2" %>

JSP 规范共定义了三种核心指令:

  • page 指令:定义当前页面的全局环境与翻译属性。
  • include 指令:在翻译阶段将外部文件的源代码静态合并到当前页面。
  • taglib 指令:在当前页面引入自定义标签库或 JSTL 标准标签库。

page 指令 ​

page 指令用于针对当前 JSP 页面设定全局属性与运行环境,是使用频率最高的指令。除 import 属性外,其余属性在同一个页面中均只能出现一次。

核心定义 ​

通常置于 JSP 文件的第一行,用于声明语言、内容类型与编码格式:

jsp
<%-- 统一设置响应字符集与页面文件编码 --%>
<%-- 告知容器以 UTF-8 方式解析源文件并输出流 --%>
<%@ page language="java" contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
编码控制属性 ​

很多乱码问题的根源在于混淆了 pageEncoding 与 contentType:

  • pageEncoding:指定 JSP 文件保存在磁盘上时采用的字符编码。JSP 引擎在翻译期依靠该属性读取并解析 .jsp 文本。
  • contentType:指定生成的 Servlet 在执行时,通过 HTTP 响应头向浏览器发送的 MIME 类型与字符集(即 response.setContentType(...))。
类库导入属性 ​

使用 import 属性可以在 JSP 翻译生成的 Servlet 中引入 Java 类。它是唯一允许在同一个页面中多次声明或通过逗号分隔多个包的属性:

jsp
<%-- 引入页面中需要使用的 Java 工具类 --%>
<%-- 翻译后将转化为 Servlet 类顶部的 import 语句 --%>
<%@ page import="java.util.List, java.util.ArrayList" %>
<%@ page import="java.util.Map" %>
运行环境属性 ​

通过调整环境属性,可以细粒度控制 JSP 翻译后的 Servlet 行为:

属性名默认值作用与机制
sessiontrue是否自动创建或获取 HttpSession。若设为 false,则页面内无法直接使用内置对象 session。
buffer8kb设置 out 内置对象(JspWriter)的输出缓冲区大小,设为 none 则关闭缓冲。
autoFlushtrue当缓冲区溢出时是否自动刷新输出流。若设为 false,缓冲区满时将抛出溢出异常。
isELIgnoredfalse是否忽略 EL 表达式 ${...} 。若设为 true,EL 表达式将被当作普通纯文本输出。
trimDirectiveWhitespacesfalse是否自动清除指令元素在编译后留下的多余空行,设为 true 能显著压缩 HTML 体积。
异常体系 ​

page 指令提供了服务端统一错误捕获与跳转机制,由两项属性配合使用:

  1. 业务页面触发异常:在业务页面的 errorPage 属性中指定发生未捕获异常时的目标处理页面。

  2. 错误页面处理异常:在目标页面的 isErrorPage 属性中显式设为 true,激活内置对象 exception。

    jsp
    <%-- 业务页面:捕获未处理异常并跳转到错误提示页 --%>
    <%@ page errorPage="/common/error.jsp" %>
    
    <%-- 错误处理页面(error.jsp):声明自己为错误处理页 --%>
    <%@ page isErrorPage="true" contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>

include 指令 ​

include 指令用于实现静态包含,在翻译阶段将另一个文件的内容原封不动地合并到主页面中。

静态包含 ​

语法格式为 <%@ include file="relativeURL" %>。通常用于拆分整个站点通用的 UI 部件,如顶部页头、底部版权栏或全局样式引入:

jsp
<%-- 在页面结构中静态嵌入公共导航与页脚 --%>
<%@ include file="/common/header.jsp" %>
<div class="main-content">业务内容区域</div>
<%@ include file="/common/footer.jsp" %>
编译机理 ​

理解静态包含的关键在于其所处的生命周期节点:

  1. 编译期合并:JSP 引擎在解析主页面时,一旦遇到 <%@ include %>,会立即读取被包含文件的纯文本内容,直接拼接到主页面的该指令所在位置。

  2. 单一 Servlet 生成:合并后的所有内容作为一个整体,被统一翻译成唯一一个 .java 源文件,并最终编译为单独一个 .class 字节码文件。

  3. 性能优势:由于合并动作仅在首次请求或文件修改后的翻译期触发一次,运行期无需在不同 Servlet 之间进行方法调度或请求派发,执行效率极高。

注意事项 ​

静态包含由于源码级别的直接拼接,容易引发以下问题:

  • 局部变量名冲突:被包含文件与主页面中的 <% ... %> 代码片段最终处于同一个 _jspService() 方法体内。如果在两个页面中声明了同名的局部变量,会导致编译失败(重复变量定义错误)。
  • HTML 骨架完整性:被包含的子文件不应包含独立的 <html>、<head>、<body> 标签,仅保留局部的 <div> 或功能代码片段即可,否则拼接后会破坏 HTML 语义结构。

taglib 指令 ​

taglib 指令用于在 JSP 页面中引入第三方标签库或自定义标签库(如 JSTL 标准标签库),以彻底取代传统的 Java 脚本片段。

标签库引入 ​

语法格式由两个核心属性构成:

jsp
<%-- 引入 JSTL 核心库并指定在当前页面使用的前缀 --%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

核心属性:

  • uri(Uniform Resource Identifier):标签库描述文件(.tld 文件)中定义的唯一命名空间标识符。容器通过该 URI 寻址并加载标签定义。
  • prefix(前缀):在当前 JSP 页面中调用该标签库中各标签时的简写前缀,相当于 XML 命名空间的前缀标识。例如指定为 c 时,调用格式即为 <c:if> 或 <c:forEach>。
应用场景 ​

引入标签库后,页面的控制流和数据展示可以直接以标准 XML 标签风格编写,不再需要书写复杂的 <% ... %> 脚本:

jsp
<%-- 使用 JSTL 标签输出数据并自动进行 HTML 转义防注入 --%>
<c:out value="${user.username}" default="游客" />

页面配置 ​

在承载异常展示的 JSP 页面顶部,需合理组合 page 指令的各类属性,保证中文正常渲染且能够调用内置异常对象:

jsp
<%-- 声明异常页并引入数据处理集合类 --%>
<%-- 激活内置 exception 对象以便打印堆栈信息 --%>
<%@ page language="java" contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"
     isErrorPage="true" import="java.util.List, java.io.PrintWriter" %>

动作元素 ​

动作元素基于 XML 标签语法,用于在页面请求处理阶段触发特定服务端行为。

语法格式为 <jsp:动作名 属性="值">...</jsp:动作名>。

核心动作 ​

  • <jsp:include>:动态包含外部页面。

  • <jsp:forward>:服务端请求转发,将当前请求转交至另一个资源处理。

  • <jsp:param>:作为包含或转发动作的子标签,负责向目标页面传递 HTTP 请求参数。

    jsp
    <%-- 动态包含外部资源并传递业务参数 --%>
    <jsp:include page="/header.jsp">
      <jsp:param name="pageTitle" value="Dashboard" />
    </jsp:include>

包含模式对比 ​

对比维度静态包含(<%@ include %>)动态包含(<jsp:include>)
生效时机编译期(翻译阶段直接拼接源码)运行期(请求到达时动态执行)
编译产物多个文件合并编译为一个 .class各自独立编译为单独的 .class
参数传递不支持动态传参(共享同一个方法内局部变量)支持使用 <jsp:param> 传递参数

实战:模板渲染 ​

在传统的 JavaWeb 业务开发中,通常借助 JSP 基础语法将数据持久层查询结果动态渲染到 HTML 结构中。

利用代码片段控制循环,利用表达式脚本完成数据项填充:

jsp
<%-- 驱动底层查询并在脚本片段中控制遍历流 --%>
<%-- 动态交织 HTML 标签输出表格明细 --%>
<%
  ResultSet rs = stmt.executeQuery(sql);
  while (rs.next()) {
%>
<tr>
  <td><%= rs.getInt("category_id") %></td>
  <td><%= rs.getInt("total_count") %></td>
</tr>
<%
  }
%>

内置对象 ​

总览 ​

JSP 内置对象(又称隐式对象,Inbuild Object) 是 Web 容器(如 Tomcat)在将 .jsp 源码翻译为 Servlet 源文件时,自动在 _jspService() 核心方法内部声明并初始化的局部变量。

因为变量在方法体顶部已默认完成赋值,所以在 JSP 的代码片段(<% ... %>)和表达式脚本(<%= ... %>)中,无需通过 new 手动实例化,也无需声明类型,即可直接调用其方法。

java
// 容器在 _jspService 方法内部预置的局部变量定义
// 无需显式实例化即可在脚本与表达式中直接使用
PageContext pageContext = null;
HttpSession session = null;
ServletContext application = null;
ServletConfig config = null;
JspWriter out = null;
Object page = this;

JSP 规范共定义了 9 个内置对象,按其功能职责可划分为四大类别:

  • 输入/输出对象:request、response、out。
  • 作用通信对象:pageContext、session、application。
  • 环境配置对象:page、config。
  • 异常处理对象:exception。
对象名对应 Java 类型职责边界与典型用途
requestHttpServletRequest提取请求参数、获取请求头、获取 Cookie、存取请求域属性
responseHttpServletResponse设置响应状态码、写入响应头、发送重定向、添加 Cookie
sessionHttpSession维持用户会话状态,用于存储登录态与跨请求临时数据
applicationServletContextWeb 应用全局上下文,所有用户与 Servlet 共享的全局容器
outJspWriter带有缓冲区特性的字符输出流,负责向页面写入 HTML 内容
pageContextPageContext当前页面的上下文中心,可存取四大作用域并直接获取其他八大对象
pageObject指向当前 Servlet 实例自身(相当于 Java 中的 this)
configServletConfig获取当前 JSP 的 Servlet 初始化参数及 ServletContext
exceptionThrowable异常实例,仅在设置了 isErrorPage="true" 的页面中合法有效

输入输出 ​

输入与输出对象负责处理客户端与服务端之间的 HTTP 报文交互以及最终页面的字符流输出。

request 对象 ​

  • 真实类型:javax.servlet.http.HttpServletRequest
  • 职责边界:封装客户端发起的单次 HTTP 请求的全部信息,包括 URL 路径、请求参数、请求头、Cookie 和上传的数据流。同时,它本身也是一个请求范围的属性载体。
  • 核心方法:
    • getParameter(String name):获取单值请求参数。

    • getParameterValues(String name):获取多值参数(如复选框)。

    • setAttribute(String name, Object o):在请求域绑定数据。

    • getRequestDispatcher(String path):获取请求转发器。

response 对象 ​

  • 真实类型:javax.servlet.http.HttpServletResponse

  • 职责边界:封装服务端即将回传给浏览器的 HTTP 响应,用于指定状态码、写入响应头、设置 Cookie 以及发起客户端重定向。

  • 核心方法:

    • setContentType(String type):设置响应的 MIME 类型与字符集。

    • sendRedirect(String location):向客户端发送 302 重定向指示。

    • addCookie(Cookie cookie):向客户端写入 Cookie 凭据。

    java
    // 读取客户端请求参数并完成合法性校验
    // 校验失败后主动重定向至错误反馈页面
    String username = request.getParameter("username");
    if (username == null || username.trim().isEmpty()) {
      response.sendRedirect("/login.jsp?error=empty");
    }

out 对象 ​

  • 真实类型:javax.servlet.jsp.JspWriter
  • 职责边界:带有缓冲机制的字符输出流,负责向响应体写入动态拼接的 HTML 内容。
  • 输出缓冲机理:out 对象内部持有一个输出缓冲区(默认 8KB)。与之相对,通过 response.getWriter() 获取的 PrintWriter 对象则是直接输出到 Servlet 引擎的缓冲区。若在同一页面混用两者,PrintWriter 中的数据会先于 out 中的数据发送给客户端,导致页面显示顺序错乱。

作用通信 ​

此类对象负责在不同作用域和组件生命周期之间共享上下文数据。

pageContext 对象 ​

  • 真实类型:javax.servlet.jsp.PageContext
  • 职责边界:当前 JSP 页面的核心上下文管理者,在内置对象中扮演“总管”角色。
  • 双重核心职能:
    1. 统一操作四大作用域:

      • 通过 setAttribute(key, value, scope) 可直接向任意指定的作用域存取数据;
      • 通过 findAttribute(key) 会按照 page -> request -> session -> application 的顺序从小到大逐级向上寻址,直到命中属性为止。
    2. 获取其他内置对象:提供诸如 getRequest()、getResponse()、getSession()、getOut() 等方法。在编写自定义标签或通用拦截组件时,只需传入 pageContext 即可调用整个运行环境。

      java
      // 通过 pageContext 在特定域存储属性
      // findAttribute 按照 page 到 application 逐级向上检索
      pageContext.setAttribute("userRole", "admin", PageContext.REQUEST_SCOPE);
      Object role = pageContext.findAttribute("userRole");

session 对象 ​

  • 真实类型:javax.servlet.http.HttpSession

  • 职责边界:代表单个客户端与服务端建立的会话状态通道,依靠 Cookie 中的 JSESSIONID 跟踪多步请求,常用于维护登录态、购物车或跨步骤流程。

  • 核心方法:

    • setAttribute(String name, Object value):在当前会话中暂存数据。

    • getAttribute(String name):读取当前会话中的对象。

    • invalidate():强制使当前 Session 立即失效并释放资源。

  • 使用前提:当前页面的 page 指令未设置 session="false"。

application 对象 ​

  • 真实类型:javax.servlet.ServletContext
  • 职责边界:代表整个 Web 应用的全局上下文。随容器启动而创建,随应用卸载而销毁,被应用内的所有在线用户和所有组件全局共享。
  • 核心场景:读取 web.xml 全局初始参数、维护站点全局在线统计、存放应用级静态缓存。

环境配置 ​

环境配置对象主要提供当前 Servlet 实例本身的信息以及部署时的配置参数。

page 对象 ​

  • 真实类型:java.lang.Object(底层指向当前 Servlet 的实例引用 this)
  • 职责边界:代表当前正在执行的 JSP 转换类自身。
  • 开发约束:在翻译生成的代码中,其声明为 Object page = this;。由于声明类型为通用的 Object,在没有强转为底层 Servlet 类的前提下只能调用 Object 类的通用方法(如 toString、hashCode),因此在实际日常开发中极少直接调用。

config 对象 ​

  • 真实类型:javax.servlet.ServletConfig
  • 职责边界:封装当前 JSP 在 Web 容器中的配置参数。
  • 核心方法:
    • getServletName():获取当前 JSP 注册的 Servlet 名称。

    • getInitParameter(String name):获取在 web.xml 中通过 <init-param> 为该 JSP 配置的私有初始化参数。

    • getServletContext():获取全局的 ServletContext 句柄。

异常处理 ​

专门用于捕获并展示应用运行时抛出的未受检异常。

exception 对象 ​

  • 真实类型:java.lang.Throwable

  • 职责边界:封装页面执行时发生的异常实例,用于向运维人员或开发人员输出错误跟踪信息。

  • 激活条件:必须在页面顶部显式配置 <%@ page isErrorPage="true" %>。如果未做该配置,JSP 编译器在生成 _jspService时不会声明并注入exception 变量,在页面中强行调用将产生编译期未定义变量错误。

    jsp
    <%-- 声明为错误捕获页面以激活内置 exception 对象 --%>
    <%-- 输出异常具体摘要信息至前台展示 --%>
    <%@ page isErrorPage="true" contentType="text/html;charset=UTF-8" %>
    <div class="error-panel">
      <h3>系统运行异常:<%= exception.getMessage() %></h3>
    </div>

四大作用域 ​

作用域概述 ​

在 JavaWeb 和 JSP 中,作用域(Scope) 是指数据在服务端的存活时间与可见范围。

HTTP 协议本身是无状态的,为了在不同的页面、请求、会话甚至整个应用之间传递与共享数据,Web 容器提供了四种不同生命周期的容器对象。这些对象均实现了标准的属性绑定接口,通过统一的键值对(Key-Value)方式管理内部状态。

作用域总览:

作用域名称对应内置对象真实接口/类生命周期与可见性典型应用场景
pagepageContextjavax.servlet.jsp.PageContext当前页面运行期,页面执行完毕立即销毁页面内部临时计算、标签间传值
requestrequestjavax.servlet.http.HttpServletRequest单次 HTTP 请求链,服务器转发可共享控制器向视图传递数据
sessionsessionjavax.servlet.http.HttpSession客户端会话期,跨多次请求有效用户登录状态、购物车、多步表单
applicationapplicationjavax.servlet.ServletContextWeb 应用运行期,所有用户全局共享系统全局配置、在线人数统计、系统级缓存

page 域 ​

生命周期 ​

page 域是四大作用域中生命周期最短、可见范围最小的一个。

  1. 创建时机:客户端请求到达目标 JSP 页面,JSP 引擎开始调用 _jspService() 方法时创建。

  2. 销毁时机:当前 JSP 页面执行完毕并向客户端输出响应流后立即销毁。

  3. 边界约束:哪怕是在当前页面内通过服务端请求转发(forward)或包含(include)跳转到另一个页面,新页面也无法访问原页面的 page 域属性。

核心特点 ​

page 域由内置对象 pageContext 直接管理。在单页面执行过程中,它的变量仅在当前物理页面内可见,不同并发线程之间相互隔离,具备天然的线程局部安全性。

典型应用 ​

通常用于页面局部的复杂逻辑计算、自定义标签处理,或作为局部计算结果的中转缓存。

java
// 在当前页面作用域暂存局部运算中间结果
// 页面渲染结束或跳转后该属性将随 pageContext 一同回收
pageContext.setAttribute("tempResult", "step1_completed");
String status = (String) pageContext.getAttribute("tempResult");

request 域 ​

生命周期 ​

request 域绑定于单次 HTTP 请求的全过程。

  1. 创建时机:客户端向服务器发起 HTTP 请求时,Web 容器创建 HttpServletRequest 实例。

  2. 销毁时机:服务端处理完该请求并彻底完成响应回传后,该请求对象被容器回收销毁。

  3. 流转范围:在同一个请求生命周期内,无论经过多少个 Filter 拦截器、Servlet 控制器,或者通过请求转发传递到多少个 JSP 页面,这些组件操作的均是同一个 request 实例。

转发与重定向 ​

request 域能否保留数据的关键在于页面跳转方式:

  • 服务端请求转发(forward):属于一次 HTTP 请求,由服务器内部调度目标资源,数据完整保留。
  • 客户端重定向(redirect):浏览器接收到 302 响应后发起第二次全新的 HTTP 请求,旧请求及其挂载的数据彻底失效。

典型应用 ​

作为 MVC 架构中 Servlet 控制器向 JSP 视图传递查询结果的标准载体。

java
// 控制器将业务处理结果写入请求作用域
// 通过服务端转发方式交由目标 JSP 视图完成表格渲染
request.setAttribute("queryResult", resultList);
request.getRequestDispatcher("/views/employee_list.jsp").forward(request, response);

session 域 ​

生命周期 ​

session 域代表某个特定用户与服务器之间建立的会话通道。

  1. 创建时机:用户首次访问服务器(且请求中需要创建会话)时,容器为其分配一个唯一的 HttpSession 实例与会话标识(JSESSIONID)。

  2. 销毁时机:

    • 会话达到预设的非活动超时时间(如 web.xml 中配置的 30 分钟)。
    • 服务端主动调用 session.invalidate() 强制注销。
    • Web 服务器异常关闭或重启(未配置会话钝化持久化时)。
  3. 流转范围:同一个浏览器窗口在未关闭且会话未超时前,发起的多次独立 HTTP 请求均共享同一个 session 实例。

会话跟踪原理 ​

HTTP 协议本身无连接状态,Web 容器底层默认依赖 Cookie 机制实现会话跟踪:

  1. 服务端创建 session 后,生成唯一的哈希字符串 JSESSIONID。

  2. 通过 HTTP 响应头 Set-Cookie: JSESSIONID=... 下发给客户端浏览器。

  3. 浏览器在此后的每次请求中自动携带该 Cookie,服务端依据该标识在内存映射表中锁定对应的 session 对象。

典型应用 ​

跨多个页面或多次交互保持用户专属状态,如身份验证凭证、权限上下文、多步骤表单提交等。

java
// 用户鉴权通过后将登录凭证持久化在会话域中
// 当用户点击退出按钮时主动调用注销接口清除上下文
session.setAttribute("loginUser", userProfile);
session.invalidate();

application 域 ​

生命周期 ​

application 域(即 ServletContext) 是整个 Web 应用中生命周期最长、可见范围最大的容器。

  1. 创建时机:Web 容器启动并加载该 Web 应用时由容器实例化。

  2. 销毁时机:Web 应用被卸载(undeploy)或 Web 容器正常关闭时销毁。

  3. 流转范围:它独立于具体的请求与会话,属于应用级别的全局共享空间。应用内的所有用户、所有并发请求以及所有后台 Servlet 均访问同一个单例对象。

全局共享机制 ​

由于 application 域被所有并发线程无差别共享,任何对其中可变对象的写操作都必须严格考虑线程安全问题。若无同步控制,极易引发脏读或竞态条件。此外,该作用域内不应挂载过大或长期不用的对象,否则会导致服务端堆内存无法回收,引发内存泄漏。

典型应用 ​

存放系统初始化参数、全局字典表缓存、系统在线用户总数统计等。

java
// 在应用作用域中维护全局系统在线总人数
// 任何在线用户访问时均可读取并感知该统计数值
servletContext.setAttribute("onlineCount", activeCounter.incrementAndGet());
Integer total = (Integer) servletContext.getAttribute("onlineCount");

寻址机制 ​

寻址顺序 ​

当开发者需要读取某个属性值,但未明确指定它存储在哪个作用域时,可以通过 pageContext.findAttribute(String name) 方法进行就近查找。

寻址过程遵循自小到大、逐级向上的冒泡顺序:

  1. 优先检索 page 域:若命中则立即返回,终止查找。

  2. 向上检索 request 域:若 page 域未找到,则检查当前请求域。

  3. 向上检索 session 域:若 request 域未找到,且当前会话有效,则检查会话域。

  4. 最后检索 application 域:若前三个作用域均未命中,最后到全局应用域查找;若依然不存在,则返回 null。

属性屏蔽效应 ​

若在较小作用域与较大作用域中存放了同名键(如 page 和 request 中均存在 username),调用 findAttribute 时将只能获取到较小作用域的值,这种现象称为“属性屏蔽”。为了避免隐式覆盖带来的排查困难,在多模块协作中建议使用作用域前缀精准读取。

java
// 在不同作用域写入同名属性验证就近寻址规则
// findAttribute 将优先返回 page 域设置的值
pageContext.setAttribute("siteRole", "localOperator");
request.setAttribute("siteRole", "superAdmin");

Object finalRole = pageContext.findAttribute("siteRole");